﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using PSIEpics;
using string_short;
using System.Threading;
using System.IO;

using jtext_EpicsClient;

//this is the EpicsJecektClient lib
//it communicate with the server via epics 
//it use JEClient to access the PVs
//Notice that the IN and OUT is for the server 
//so here msg+in is acctruly the message to be sebnt out to the server
//msg_out is the msg sent  out from the server to here

//!!!!big changes the short array used to store byte or string is now using sbyte
//the lenth which used to be in short now is in byte

namespace EpicsJecketClient
{
    /// <summary>
    ///this is the EpicsJecektClient lib,
    ///it communicate with the server via epics ,
    ///it use JEClient to access the PVs,
    ///Notice that the IN and OUT is for the server ,
    ///so here pv:msg+in is acctruly the message to be sent out to the server,
    ///pv:msg_out is the msg sent  out from the server to here,
    /// </summary>
    public class EJClient
    {

        const bool IFLOG = true;

        /// <summary>
        /// the lenth limit for each message
        /// </summary>
        const int MSG_LEN = 1024;
        /// <summary>
        /// when server broadcast the client name is set to this
        /// </summary>
        const string BROADCAST = "ALL"; //server use this client nameout when broad casting

        /// <summary>
        /// it uses JEClient to de the epics job
        /// <seealso cref="JEClient"/>
        /// </summary>
        JECLient MyClient;

        /// <summary>
        /// the server you wanted to connect ,only one for one client
        /// </summary>
        string ServerName;
        /// <summary>
        /// the name of this client
        /// </summary>
        string ClientName;
        sbyte [] ClientNameSbyte;
        /// <summary>
        /// if this server-client useds byte message or string
        /// </summary>
        public bool ByteCMD;   //if the cmd is in byte form or else in ascii string
        /// <summary>
        /// when reply message comes how should i react 
        /// <para>0=ignore any,1=reise envent not ack,2=rise event ack(me),3=rise event not ack(others)</para>
        /// </summary>
        int reply_Ack_Lev;  //0=ignore any,1=rise event when it's for me, but not ack ,2=rise event when it's for me, and ack,3=rise event not ack(others)
        /// <summary>
        /// the status of the client,0=idle,1=sending;2=sent but not ack , -1 can not connect ,-2 unkonw
        /// </summary>
        public int ClientSt;   //the status of the client,0=idle,1=sending;2=sent but not ack 《0 abnormal
        int MSGLen;

        byte[] MSGByte;     //the byte array to store the message
        string MSGString;   //the string to store the message
        //short[] MSGShort;   //this is importent ,this hold the short arrary directly get from the array recode in epics server
        double LastMsgOutReq;
        //public int MsgInterval;

        byte[] MSGOUTByte;
        string MSGOUTString;   

        #region pvname
        //pv names
        string PV_MSG_IN_Req;
        string PV_MSG_OUT_Req;
        string PV_busy;
        string PV_MSG_Len;  //the byte message lenth ,notice that this lenth is in  byte
        string PV_MSG_Len_OUT;
        string PV_MSG_IN;
        string PV_MSG_OUT;
        string PV_ClientName;
        string PV_ClientName_Out;
        #endregion

        /// <summary>
        /// handles the event that server has acknowleged you message
        /// </summary>
        /// <param name="state">The state.</param>
        public delegate void Message_ackHandler(object state);   //when the server ack the message
        /// <summary>
        /// when server has acknowleged you message this is called,so if you need handle that regsit this ,like a event
        /// </summary>
        public event Message_ackHandler OnMessage_Ack;

        /// <summary>
        /// handles the event that server has sent you a message
        /// <seealso cref="reply_Ack_Lev"/>
        /// </summary>
        /// <param name="MSG">The MSG.</param>
        public delegate void Message_ArriveHandler(object MSG);     //when server sent you a message


        /// <summary>
        /// when server sent you a message this is called,so if you need handle that regsit this ,like a event
        /// </summary>
        public event  Message_ArriveHandler OnMessage_Arrive;

        //public delegate void ClientStatusChangeHandler(int Status);
        //public event ClientStatusChangeHandler OnClientStatusChange; //when client status changed this will rise

        /// <summary>
        /// becouse the Monitor change invoke is in another context of the current thread so lots of things can not be done here you have to create another thread when msg arrived
        /// </summary>
        /// <param name="newvalue">The newvalue.</param>
        delegate void  ArvInvk(object newvalue);  //becouse the Monitor change invoke is in another context of the current thread so lots of things can not be done here you have to create another thread when msg arrived

        /// <summary>
        /// becouse the Monitor change invoke is in another context of the current thread so lots of things can not be done here you have to create another thread when server acknowleged you message
        /// </summary>
        /// <param name="newvalue">The newvalue.</param>
        delegate void  AckInvk(object newvalue); //the same as above used when ack 


        //public void ConnectStatusChanged(EpicsChannel tEchannel,ChannelStatus tChannelstat)
        //{
        //    if (tChannelstat == ChannelStatus.CONNECTED)
        //    {
        //        ClientSt = 0;
        //    }
        //    if (OnClientStatusChange != null)
        //    {
        //        OnClientStatusChange(ClientStatus); //if the connection status changed rise evnnt
        //    }
        //}


        /// <summary>
        /// the status of the client,0=idle,1=sending;2=sent but not ack 《0 abnormal
        /// <para></para>
        /// </summary>
        public int ClientStatus
        {
            get 
            {
                try
                {
                    if (MyClient[PV_MSG_IN_Req].Status != ChannelStatus.CONNECTED )
                    {
                        WriteLog("getting status from server", IFLOG);
                        ClientSt = -1;
                    }
                    else if (ClientSt == -1)
                    {
                        ClientSt = 0;
                    }
                }
                catch
                {
                    //throw new SystemException("shit happend");
                }
                return ClientSt; 
            }
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="EJClient"/> class.
        /// </summary>
        /// <param name="servername">the server you wanted to connect ,only one for one client.</param>
        /// <param name="clientName">the name of this client.</param>
        /// <param name="isByte">if set to <c>true</c> then the server uses byte[] message</param>
        /// <param name="ack_lev">The acknowlege level,0=ignore any,1=reise envent not ack,2=rise event ack(me),3=rise event not ack(others).</param>
        public EJClient(string servername, string clientName,bool isByte,int ack_lev)
        {

            ServerName = servername;
            ClientName = clientName;
            ClientNameSbyte = Str_Short.StringToSByte(clientName);
            ClientSt = -2;  //abnormal
            ByteCMD = isByte;
            reply_Ack_Lev = ack_lev;
            //LastMsgOutReq = 0;
            //MsgInterval = 50;   //youcan change this before server start

            PV_busy = servername + ":BUSY";
            PV_MSG_IN_Req = servername + ":MSG_IN_REQ";
            PV_MSG_OUT_Req = servername + ":MSG_OUT_REQ";
            PV_MSG_Len = servername + ":MSG_LEN";
            PV_MSG_Len_OUT = servername + ":MSG_LENOUT";

            PV_ClientName = servername + ":CLIENTNAME";
            PV_MSG_OUT = servername + ":MSG_OUT";
            PV_ClientName_Out = servername + ":CLIENTNAMEOUT";
            PV_MSG_IN = servername + ":MSG_IN";

            MyClient = new JECLient();


        }

        /// <summary>
        /// Disposes the server.and the client,and dis connect;
        /// </summary>
        public void DisposeServer()
        {
            MyClient.DisposeAll();
            ClientSt = -2;
            //if (OnClientStatusChange != null)
            //{
            //    OnClientStatusChange(ClientStatus); 
            //}


        }

        /// <summary>
        /// Starts the client.connect to the server then it can work
        /// </summary>
        public void StartClient()
        {
            //add pvs
            MyClient.AddChannel(PV_busy);
            MyClient.AddChannel(PV_MSG_IN_Req);
            MyClient.AddChannel(PV_MSG_OUT_Req);
            MyClient.AddChannel(PV_MSG_Len);
            MyClient.AddChannel(PV_MSG_Len_OUT);

            MyClient.AddChannel<sbyte>(PV_ClientName);
            MyClient.AddChannel<sbyte>(PV_MSG_OUT);
            MyClient.AddChannel<sbyte>(PV_ClientName_Out);
            MyClient.AddChannel<sbyte>(PV_MSG_IN);

            ClientSt = 0;
            //if (OnClientStatusChange != null)
            //{
            //    OnClientStatusChange(ClientStatus); //if the connection status changed rise evnnt
            //}
            //add monitor
            MyClient[PV_MSG_OUT_Req].MonitorChanged += new EpicsDelegate(this.MonitorMessageArrive);
            MyClient[PV_MSG_IN_Req].MonitorChanged += new EpicsDelegate(this.MonitorAck);

            //MyClient[PV_MSG_IN_Req].StatusChanged += new EpicsStatusDelegate(ConnectStatusChanged);
        }


        #region sned

        /// <summary>
        /// send out  the specified message to the server.
        /// </summary>
        /// <param name="message">The message.</param>
        /// <returns>-1 this client is busy try later;-3 server busy timeout;-2 maybe disconnected;0 ok</returns>
        public int  Msg_send(string message)
        {
            //this will block till the send id finish
            int try_times=0;
            Random waittime=new Random();
            int  msglen;
            if (ClientSt != 0)
            {
                WriteLog("server busy ,exit sending", IFLOG);
                return -1;  //client busy
            }
            else
            {
                ClientSt = 1;
                try
                {
                    WriteLog("getting if busy", IFLOG);
                    while (MyClient[PV_busy].Get<double>() != 0)
                    {
                        WriteLog("getting if busy,OK", IFLOG);
                        Thread.Sleep(waittime.Next(30, 500));
                        try_times++;
                        if (try_times > 8)
                        {
                            WriteLog("server time out", IFLOG);
                            ClientSt = 0;
                            return -3;  //server busy timeout
                        }
                    }//while

                    //msg = Str_Short.StringToSByte(message);
                    WriteLog("putting client name", IFLOG);
                    MyClient[PV_ClientName].Put(ClientNameSbyte);   //set the name
                    //MyClient[PV_MSG_IN].Put(msg);
                    //MyClient[PV_MSG_Len].Put(msg.Length);
                    WriteLog("putting message", IFLOG);
                    msglen = MyClient.SetString(PV_MSG_IN, message); //set message
                    WriteLog("putting len", IFLOG);
                    MyClient[PV_MSG_Len].Put(msglen);   //set meg len
                    WriteLog("putting Req in", IFLOG);
                    MyClient[PV_MSG_IN_Req].Put(1);
                    ClientSt = 2;
                }
                catch
                {
                    ClientSt = -1;
                    return -2;
                }
                return 0;   //send finished
            }
        }

        /// <summary>
        /// send out  the specified message to the server.
        /// </summary>
        /// <param name="message">The message.</param>
        /// <returns>-1 this client is busy try later;-3 server busy timeout;-2 maybe disconnected;0 ok</returns>
        public int Msg_send(byte[] message)
        {
            int try_times = 0;
            Random waittime = new Random();
            int msglen;
            if (ClientSt != 0)
            {
                return -1;  //client busy
            }
            else
            {
                ClientSt = 1;
                try
                {
                    WriteLog("getting busy", IFLOG);
                    while (MyClient[PV_busy].Get<double>() != 0)
                    {
                        WriteLog("getting ok", IFLOG);
                        Thread.Sleep(waittime.Next(10, 100));
                        try_times++;
                        if (try_times > 5)
                        {
                            ClientSt = 0;
                            return -3;  //server busy timeout
                        }
                    }//while

                    //msg = Str_Short.StringToSByte(message);
                    WriteLog("putting name", IFLOG);
                    MyClient[PV_ClientName].Put(ClientNameSbyte);   //set the name
                    //MyClient[PV_MSG_IN].Put(msg);
                    //MyClient[PV_MSG_Len].Put(msg.Length);
                    WriteLog("putting msg", IFLOG);
                    msglen = MyClient.SetByte(PV_MSG_IN, message); //set message
                    WriteLog("putting msg len", IFLOG);
                    MyClient[PV_MSG_Len].Put(msglen);   //set meg len
                    WriteLog("putting in req", IFLOG);
                    MyClient[PV_MSG_IN_Req].Put(1);
                    ClientSt = 2;
                }
                catch 
                {
                    ClientSt = -1;
                    return -2;
                }
                return 0;   //send finished
            }
        }
        #endregion


        /// <summary>
        /// when the message you sent is Ack by server ,this is called.
        /// </summary>
        /// <param name="newValue">The new value.</param>
        private void AckInvoke(object newValue) //acknowleged by server
        {
            //string client;
            //try
            //{
            //    client = MyClient.GetString(PV_ClientName, ClientNameSbyte.Length); //add try block
            //}
            //catch 
            //{
            //    client = "NULL";
            //}
            //if (client == ClientName) //it's me just sent a msg
            //{
            if (ClientSt == 0)
                return;

            if (ClientSt == 2)
                {
                    ClientSt = 0;
                    //if (OnClientStatusChange != null)
                    //{
                    //    OnClientStatusChange(ClientStatus); //if the connection status changed rise evnnt
                    //}
                }
                if (OnMessage_Ack != null)
                {
                    OnMessage_Ack(null);
                }
            //}
        }

        /// <summary>
        /// when message from server is arrived this is called
        /// </summary>
        /// <param name="newValue">The new value.</param>
        private void  ArriveInvoke(object newValue)
        {
          
            //check if message arriverd
            string client;
            object  msg;
            //string Smsg;
            double  msglen2;
            int msglen;
            if ((double)newValue == 1)
            {
                switch (reply_Ack_Lev)
                {
                    case 0:
                        {
                            return;
                        }
                    case 1: //rise event when it's for me, but not ack  (not recommend)
                        {
                            //client = MyClient.GetString(PV_ClientName_Out, ClientNameSbyte.Length);
                            WriteLog("get client name", IFLOG);
                            client = MyClient.GetString(PV_ClientName_Out);
                            if (client == ClientName || client==BROADCAST )
                            {
                                WriteLog("getting msg len out", IFLOG);
                                msglen2 = (double)(MyClient[PV_MSG_Len_OUT].Get());
                                msglen = (int)msglen2;
                                if (ByteCMD != true)
                                {
                                    WriteLog("getting out msg", IFLOG);
                                    msg = MyClient.GetString(PV_MSG_OUT, msglen);
                                }
                                else
                                {
                                    WriteLog("getting out msg", IFLOG);
                                    msg = MyClient.GetByte(PV_MSG_OUT, msglen);
                                }
                                //call, invoke, rise message here
                                if (OnMessage_Arrive != null)
                                {
                                    OnMessage_Arrive(msg);
                                }
                            }
                            break;
                        }
                    case 2: //rise event when it's for me and ack
                        {
                            
                            //client = MyClient.GetString(PV_ClientName_Out, ClientNameSbyte.Length);
                            client = MyClient.GetString(PV_ClientName_Out);
                            if (client == ClientName || client==BROADCAST)
                            {
                                WriteLog("getting msg len out", IFLOG);
                                msglen2 = (double)(MyClient[PV_MSG_Len_OUT].Get());
                                msglen =(int) msglen2;
                                if (ByteCMD != true)
                                {
                                    WriteLog("getting out msg", IFLOG);
                                    msg = MyClient.GetString(PV_MSG_OUT, msglen);
                                }
                                else
                                {
                                    WriteLog("getting out msg", IFLOG);
                                    msg = MyClient.GetByte(PV_MSG_OUT, msglen);
                                }

                                if (client != BROADCAST)
                                {
                                    MyClient[PV_MSG_OUT_Req].Put(0);    //ack
                                }
                                //call, invoke, rise message here
                                if (OnMessage_Arrive != null)
                                {
                                    OnMessage_Arrive(msg);
                                }
                            }
                            break;
                        }
                    case 3:  //rise event even it's not for me ,and ack if it's for me 
                        {

                            client = MyClient.GetString(PV_ClientName_Out, ClientNameSbyte.Length);
                            msglen2 = (double)(MyClient[PV_MSG_Len_OUT].Get());
                            msglen = (int)msglen2;
                            if (ByteCMD != true)
                            {
                                msg = MyClient.GetString(PV_MSG_OUT, msglen);
                            }
                            else
                            {
                                msg = MyClient.GetByte(PV_MSG_OUT, msglen);
                            }
                            //call, invoke, rise message here
                            if(OnMessage_Arrive!=null)
                            {
                                OnMessage_Arrive(msg);
                            }
                            if (client == ClientName)
                            {
                                MyClient[PV_MSG_OUT_Req].Put(0);//ACK
                            }
                            break;


                        }
                }
            }
            else
            {
                return;
            }
        }

        private void MonitorMessageArrive(EpicsChannel sender, object newValue)
        {
            ArvInvk a = new ArvInvk(ArriveInvoke);
            IAsyncResult asyncResult=a.BeginInvoke(newValue,null ,null );
        }


        private void MonitorAck(EpicsChannel sender, object newValue)
        {
            AckInvk a = new AckInvk(AckInvoke);
            IAsyncResult asyncResult = a.BeginInvoke(newValue, null, null);
        }
   
        private void WriteLog(string log,bool IfLog)
        {
            if (IfLog == false)
                return;
            StreamWriter Sw = new StreamWriter(AppDomain.CurrentDomain.BaseDirectory.ToString() + "log.txt",true);
     
            Sw.WriteLine(log);
            Sw.Close();
        }




    }
}
